package jroguelike.main;

import aimozg.joglfw.GLColor;
import aimozg.joglfw.JoglApplication;
import com.sun.opengl.util.texture.Texture;
import com.sun.opengl.util.texture.TextureIO;
import jroguelike.utils.ColouredString;
import jroguelike.utils.ColouredText;
import jroguelike.utils.Glyph;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import javax.media.opengl.GL;
import java.awt.event.KeyEvent;
import java.io.File;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;

import static java.awt.event.KeyEvent.*;

public final class RLWindow extends JoglApplication {
	private static final char HOTKEY_CLOSE = 'C';
	private static final char HOTKEY_OPEN = 'O';
	private static final char HOTKEY_LOOK = 'l';
	private static final char HOTKEY_DEBUG = 'D';
	private static final char HOTKEY_PICKUP = 'g';
	private static final char HOTKEY_SWAP_WEAPONS = 'z';
	private static final char HOTKEY_DROP = 'd';
	public static final char HOTKEY_INVENTORY = 'i';

	private static final char HOTKEY_DD_POSSESS = 'P';

	public static final char HOTKEY_I_DROP = 'd';
	public static final char HOTKEY_I_WEAR = 'w';
	public static final char HOTKEY_I_USE = 'u';

	////////////// GAME-RELATED

	World world;
	RLMap.FOV fov;

	Creature player() {
		return world.player();
	}

	RLMap map() {
		return world.player().map();
	}

	////////////// UI-RELATED
	boolean renderCursor = false;
	int curX, curY, curX0, curY0;
	long gameTickMs = 0;

	enum UIMode {
		ACTIVE, WAIT, SELECT_TARGET, SELECT_DIRECTION, SELECT_SUBCOMMAND, SELECT_ITEM
	}

	enum LongCommand {
		CLOSE("Close", "Close - select direction", HOTKEY_CLOSE),
		OPEN("Open", "Open - close direction", HOTKEY_OPEN),
		LOOK("Look", "Look", HOTKEY_LOOK),
		SUB_D("D+", "'D' + <?>", HOTKEY_DEBUG, HOTKEY_DD_POSSESS),
		POSSESS("Possess", "Possess - select target", HOTKEY_DD_POSSESS),
		DROP("Drop", "Drop - select item", HOTKEY_DROP),
		INVENTORY("Inventory", "Inventory: [d]rop/[w]ear/[u]se", HOTKEY_INVENTORY, HOTKEY_I_DROP, HOTKEY_I_WEAR, HOTKEY_I_USE),
		USE_ITEM("Use item", "Use - select item", HOTKEY_I_USE),
		WEAR("Wear", "Wear/take off - select item", HOTKEY_I_WEAR);
		public final String name;
		public final String uihint;
		public final char hotkey;
		public final String validConfirmation;

		LongCommand(String name, String uihint, char hotkey, char... validConfirmations) {
			this.name = name;
			this.uihint = uihint;
			this.hotkey = hotkey;
			if (validConfirmations.length == 0) this.validConfirmation = String.valueOf(hotkey);
			else this.validConfirmation = new String(validConfirmations);
		}

		public boolean validConfirmation(char c) {
			return validConfirmation.indexOf(c) >= 0;
		}
	}

	volatile UIMode uiMode = UIMode.ACTIVE;
	LongCommand longCommand = null;

	private static final int[] directionCodes = {
			VK_NUMPAD7, VK_NUMPAD8, VK_NUMPAD9,
			VK_NUMPAD4, VK_NUMPAD6,
			VK_NUMPAD1, VK_NUMPAD2, VK_NUMPAD3
	};
	public static final int[] directionDxs = {
			-1, 0, 1,
			-1, 1,
			-1, 0, 1
	};
	public static final int[] directionDys = {
			-1, -1, -1,
			0, 0,
			1, 1, 1
	};

	private static int directionIdx(KeyEvent e) {
		for (int i = 0; i < directionCodes.length; i++) {
			if (directionCodes[i] == e.getKeyCode()) return i;
		}
		return -1;
	}

	void targetModeStart() {
		uiMode = UIMode.SELECT_TARGET;
		renderCursor = true;
		curX0 = 0;
		curY0 = statusRows;
		curX = player().x();
		curY = player().y();
	}

	void targetModeStop() {
		switch (longCommand) {
			case LOOK: /* Do nothing */
				break;
			case POSSESS:
				Creature creature = map().creature(curX, curY);
				if (creature != null && !creature.isPc()) {
					world.setPc(creature);
					world.addLog(GLColor.RED, GLColor.DARKRED, "You are now playing as " + creature.name);
				}
				break;
			case CLOSE:
			case OPEN:
			case SUB_D:
			case DROP:
			case INVENTORY:
			case WEAR:
			case USE_ITEM:
				assert false : "Illegal mode/command combination";
				break;
			default:
				assert false : "Missing case";
		}
		targetModeCancel();
	}

	void targetModeCancel() {
		uiMode = UIMode.ACTIVE;
		renderCursor = false;
		longCommand = null;
	}

	void inventoryModeStart() {
		uiMode = UIMode.SELECT_ITEM;
		renderCursor = true;
		curX0 = uiInventoryX();
		curY0 = uiInventoryY();
		curX = 0;
		curY = 0;
	}

	private void inventoryModeCancel() {
		renderCursor = false;
		uiMode = UIMode.ACTIVE;
		longCommand = null;
	}

	void inventoryModeStop(char key) {
		if (longCommand == LongCommand.INVENTORY) {
			switch (key) {
				case HOTKEY_I_DROP:
					longCommand = LongCommand.DROP;
					break;
				case HOTKEY_I_USE:
					longCommand = LongCommand.USE_ITEM;
					break;
				case HOTKEY_I_WEAR:
					longCommand = LongCommand.WEAR;
					break;
				default:
					// Enter/spacebar, so do nothing
					break;
			}
		}
		int iidx = curInventoryIndex();
		boolean validItem = iidx >= 0 && iidx < player().backpack().size();
		RLItem item = validItem ? player().backpack().get(iidx) : null;
		switch (longCommand) {
			case DROP:
				if (validItem) world.tryActionDrop(player(), item);
				break;
			case USE_ITEM:
				throw new UnsupportedOperationException("Not implemented yet");//TODO depends on item usage
			case WEAR:
				//if (validItem) world.tryAction
				throw new UnsupportedOperationException("Not implemented yet");//TODO wear or take off
			case INVENTORY:
				// Enter/spacebar, so do nothing
				break;
			case CLOSE:
			case OPEN:
			case LOOK:
			case SUB_D:
			case POSSESS:
				assert false : "Illegal mode/command combination";
				break;
			default:
				assert false : "Missing case";
		}
		inventoryModeCancel();
	}

	private int curInventoryIndex() {
		return curX + curY * uiInvWidth();
	}

	@Override
	protected void keyPressed(KeyEvent e) {
		if (e.getKeyCode() == KeyEvent.VK_SHIFT) return;
		switch (uiMode) {
			case ACTIVE:
				if (player().canAct()) {
					keypressActive(e);
				} else {
					keypressWait(e);
				}
				break;
			case SELECT_TARGET:
				keypressTarget(e);
				break;
			case SELECT_DIRECTION:
				keypressDirection(e);
				break;
			case WAIT:
				keypressWait(e);
				break;
			case SELECT_SUBCOMMAND:
				keypressSubcommand(e);
				break;
			case SELECT_ITEM:
				keypressItem(e);
				break;
			default:
				assert false : "Missed case";
		}
	}

	private void keypressItem(KeyEvent e) {
		int dir = directionIdx(e);
		if (dir >= 0) {
			int nx = curX + directionDxs[dir];
			int ny = curY + directionDys[dir];
			int iidx = nx + ny * uiInvWidth();
			if (iidx >= 0 && iidx < player().backpack().size()) {
				curX = nx;
				curY = ny;
			}
			return;
		}
		if (e.getKeyCode() == KeyEvent.VK_ENTER ||
				e.getKeyCode() == KeyEvent.VK_SPACE ||
				longCommand.validConfirmation(e.getKeyChar())) {
			inventoryModeStop(e.getKeyChar());
		} else {
			world.addLog("Unknown command");
			inventoryModeCancel();
		}
	}

	private void keypressSubcommand(KeyEvent e) {
		if (e.getKeyChar() == CHAR_UNDEFINED) {
			uiMode = UIMode.ACTIVE;
			longCommand = null;
			return;
		}
		switch (e.getKeyChar()) {
			case HOTKEY_DD_POSSESS:
				longCommand = LongCommand.POSSESS;
				targetModeStart();
				break;
			default:
				world.addLog("Unknown command");
				longCommand = null;
		}

	}

	private void keypressWait(KeyEvent e) {
		switch (e.getKeyCode()) {
			case VK_ESCAPE:
				stop();
				break;
			default:
				// Do nothing
				break;
		}
	}

	private void keypressDirection(KeyEvent e) {
		int direction = directionIdx(e);
		if (direction != -1) {
			int dx = directionDxs[direction];
			int dy = directionDys[direction];
			switch (longCommand) {
				case OPEN:
					world.tryActionOpen(player(), dx, dy);
					break;
				case CLOSE:
					world.tryActionClose(player(), dx, dy);
					break;
				case LOOK:
				case SUB_D:
				case POSSESS:
				case DROP:
					assert false : "Illegal mode/command combination";
					break;
				default:
					assert false : "Missed case";
			}
		}
		longCommand = null;
		uiMode = UIMode.ACTIVE;
	}

	private void keypressTarget(KeyEvent e) {
		int direction = directionIdx(e);
		if (direction != -1) {
			int ncx = curX + directionDxs[direction];
			int ncy = curY + directionDys[direction];
			if (map().inBounds(ncx, ncy)) {
				curX = ncx;
				curY = ncy;
			}
		} else {
			if (e.getKeyCode() == VK_ENTER ||
					e.getKeyCode() == VK_SPACE ||
					longCommand.validConfirmation(e.getKeyChar())) {
				targetModeStop();
			} else {
				targetModeCancel();
			}
		}
	}

	private void keypressActive(KeyEvent e) {
		int direction = directionIdx(e);
		if (direction != -1) {
			world.playerAction(directionDxs[direction], directionDys[direction]);
			return;
		}
		switch (e.getKeyCode()) {
			case VK_ESCAPE:
				stop();
				break;
			case VK_NUMPAD5:
				world.tryActionWait(player());
				break;
			default:
				switch (e.getKeyChar()) {
					case HOTKEY_CLOSE:
						uiMode = UIMode.SELECT_DIRECTION;
						longCommand = LongCommand.CLOSE;
						break;
					case HOTKEY_OPEN:
						uiMode = UIMode.SELECT_DIRECTION;
						longCommand = LongCommand.OPEN;
						break;
					case HOTKEY_LOOK:
						longCommand = LongCommand.LOOK;
						targetModeStart();
						break;
					case HOTKEY_DEBUG:
						longCommand = LongCommand.SUB_D;
						uiMode = UIMode.SELECT_SUBCOMMAND;
						break;
					case HOTKEY_PICKUP:
						world.tryActionAutoPickup(player());
						break;
					case HOTKEY_SWAP_WEAPONS:
						world.tryActionSwapWeapons(player());
						break;
					case HOTKEY_DROP:
						longCommand = LongCommand.DROP;
						inventoryModeStart();
						break;
					case HOTKEY_INVENTORY:
						longCommand = LongCommand.INVENTORY;
						inventoryModeStart();
						break;
					default:
						world.addLog(String.format("Unknown key %c (%s)", e.getKeyChar(), KeyEvent.getKeyText(e.getKeyCode())));
						break;
				}
		}
		synchronized (this) {
			notifyAll();
		}
	}

	///////////////////////////////////
	////////////// RENDER-RELATED

	// constants
	Texture glyphtex;
	final int CHAR_WIDTH = 10;
	final int CHAR_HEIGHT = 12;
	final String GLYPH_TEXFILE = "glyphs.png";
	final int GLYPH_TEXCOLS = 16;
	final int GLYPH_TEXROWS = 21;
	final double TXMULT_X = 1.0 / GLYPH_TEXCOLS;
	final double TXMULT_Y = 1.0 / GLYPH_TEXROWS;
	final int[] GLYPH_INDEXES = new int[1 << Character.SIZE];
	@SuppressWarnings({"MagicNumber"})
	final char[] GLYPH_LIST = {
			0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f,
			0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f,
			' ', '!', '"', '#', '$', '%', '&', '\'', '(', ')', '*', '+', ',', '-', '.', '/',    //0x2*
			'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', ':', ';', '<', '=', '>', '?',    //0x3*
			'@', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', //0x4*
			'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '[', '\\', ']', '^', '_', //0x5*
			'`', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', //0x6*
			'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '{', '|', '{', '~', 0x7f,
			0x80, 0x81, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8a, 0x8b, 0x8c, 0x8d, 0x8e, 0x8f,
			0x90, 0x91, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9a, 0x9b, 0x9c, 0x9d, 0x9e, 0x9f,
			0xa0, 0xa1, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8, 0xa9, 0xaa, 0xab, 0xac, 0xad, 0xae, 0xaf,
			0xb0, 0xb1, 0xb2, 0xb3, 0xb4, 0xb5, 0xb6, 0xb7, 0xb8, 0xb9, 0xba, 0xbb, 0xbc, 0xbd, 0xbe, 0xbf,
			0xc0, 0xc1, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6, 0xc7, 0xc8, 0xc9, 0xca, 0xcb, 0xcc, 0xcd, 0xce, 0xcf,
			0xd0, 0xd1, 0xd2, 0xd3, 0xd4, 0xd5, 0xd6, 0xd7, 0xd8, 0xd9, 0xda, 0xdb, 0xdc, 0xdd, 0xde, 0xdf,
			0xe0, 0xe1, 0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea, 0xeb, 0xec, 0xed, 0xee, 0xef,
			0xf0, 0xf1, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9, 0xfa, 0xfb, 0xfc, 0xfd, 0xfe, 0xff,
			'А', 'Б', 'В', 'Г', 'Д', 'Е', 'Ж', 'З', 'И', 'Й', 'К', 'Л', 'М', 'Н', 'О', 'П',//0x10*
			'Р', 'С', 'Т', 'У', 'Ф', 'Х', 'Ц', 'Ч', 'Ш', 'Щ', 'Ъ', 'Ф', 'Ь', 'Э', 'Ю', 'Я',//0x11*
			'а', 'б', 'в', 'г', 'д', 'е', 'ж', 'з', 'и', 'й', 'к', 'л', 'м', 'н', 'о', 'п',//0x12*
			'р', 'с', 'т', 'у', 'ф', 'х', 'ц', 'ч', 'ш', 'щ', 'ъ', 'ф', 'ь', 'э', 'ю', 'я',//0x13*
			'Ё', 'ё', 0x142, 0x143, 0x144, 0x145, 0x146, 0x147, 0x148, 0x149, 0x14a, 0x14b, 0x14c, 0x14d, 0x14e, 0x14f
	};

	// interface settings
	int windowRows;
	int statusRows;
	int mapinfoRows;
	int logRows;
	int windowCols;
	int mapCols;
	int infoCols;

	int logTop() {
		return statusRows + mapinfoRows;
	}

	GLColor paneBorderColor = GLColor.DARKGRAY;

	// interface params
	int printX = 0;
	int printY = 0;

	void printTo(int x, int y) {
		printX = x;
		printY = y;
	}

	void print(GLColor fg, GLColor bg, String fmt, Object... args) {
		String text = String.format(fmt, args);
		for (char c : text.toCharArray()) {
			glyphAt(c, fg, printX, printY, bg);
			printX++;
		}
	}

	void print(int x, int y, GLColor fg, @Nullable GLColor bg, String fmt, Object... args) {
		printTo(x, y);
		print(fg, bg, fmt, args);
	}

	void print(int x, int y, GLColor fg, String fmt, Object... args) {
		print(x, y, fg, null, fmt, args);
	}

	void print(GLColor fg, String fmt, Object... args) {
		print(fg, null, fmt, args);
	}

	void printRightJustified(int x, int y, GLColor fg, GLColor bg, String fmt, Object... args) {
		String text = String.format(fmt, args);
		print(x - text.length(), y, fg, bg, text);
	}

	void glyphAt(char glyph, @NotNull GLColor color, int x, int y, @Nullable GLColor bg) {
		if (bg != null) {
			gl.glTexEnvi(GL.GL_TEXTURE_ENV, GL.GL_TEXTURE_ENV_MODE, GL.GL_DECAL);
			drawQuads(bg,
					Pt(x, y),
					Pt((x + 1), y),
					Pt((x + 1), (y + 1)),
					Pt(x, (y + 1)));
			gl.glTexEnvi(GL.GL_TEXTURE_ENV, GL.GL_TEXTURE_ENV_MODE, GL.GL_MODULATE);
		}
		int texcol = GLYPH_INDEXES[glyph] % GLYPH_TEXCOLS;
		int texrow = GLYPH_INDEXES[glyph] / GLYPH_TEXCOLS;
		double texx = texcol * TXMULT_X;
		double texy = texrow * TXMULT_Y;
		drawQuads(color,
				Tx(texx, texy), Pt(x, y),
				Tx(texx + TXMULT_X, texy), Pt((x + 1), y),
				Tx(texx + TXMULT_X, texy + TXMULT_Y), Pt((x + 1), (y + 1)),
				Tx(texx, texy + TXMULT_Y), Pt(x, (y + 1))
		);
	}

	void glyphAt(Glyph glyph, int x, int y) {
		glyphAt(glyph.character, glyph.fg, x, y, glyph.bg);
	}

	private void renderStatus() {
		printTo(0, 0);
		switch (uiMode) {
			case ACTIVE:
			case SELECT_DIRECTION:
			case WAIT:
			case SELECT_SUBCOMMAND:
				print(GLColor.GREEN, map().name);
				printX += 2;
				print(GLColor.WHITE, "Day %d, %02d:%02d:%02d",
						world.gametime.days(),
						world.gametime.hours(),
						world.gametime.minutes(),
						world.gametime.seconds());
				break;
			case SELECT_TARGET: {
				Creature creature = map().creature(curX, curY);
				RLItem item = map().item(curX, curY);
				Terrain tile = map().tile(curX, curY);
				print(GLColor.WHITE, "You see ");
				if (creature != null) {
					print(creature.getFg(), TextUtils.healthCondition(creature.hitPts, creature.hitPtsMax()) + " " + creature.name);
					if (item != null) {
						print(GLColor.WHITE, " and ");
						print(item.color, item.displayName());
					}
					print(GLColor.WHITE, " on " + tile.name);
				} else if (item != null) {
					print(item.color, item.displayName());
					print(GLColor.WHITE, " on " + tile.name);
				} else {
					print(GLColor.WHITE, tile.name);
				}
			}
			break;
			case SELECT_ITEM: {
				int iidx = curInventoryIndex();
				List<RLItem> items = player().backpack();
				if (iidx >= 0 && iidx < items.size()) {
					RLItem item = items.get(iidx);
					print(GLColor.WHITE, item.displayName());
					// TODO more detailed description
				}
			}
			break;
			default:
				assert false : "Missed case";
		}
		switch (uiMode) {
			case ACTIVE:
				break;
			case WAIT:
				printRightJustified(windowCols, 0, GLColor.BLACK, GLColor.WHITE, "WAIT...");
				break;
			case SELECT_TARGET:
			case SELECT_SUBCOMMAND:
			case SELECT_DIRECTION:
			case SELECT_ITEM:
				printRightJustified(windowCols, 0, GLColor.BLACK, GLColor.WHITE, longCommand.uihint);
				break;
			default:
				assert false : "Missed case";
		}
	}

	private void renderMap() {
		for (int i = 0; i < map().width; i++) {
			for (int j = 0; j < map().height; j++) {
				Terrain tile = map().tile(i, j);
				Creature creature = map().creature(i, j);
				RLItem item = map().item(i, j);
				boolean visible = (fov != null) && fov.visible(i, j);
				boolean los = map().checkLOS(player().x(), player().y(), i, j);
				GLColor bg = visible ? null : GLColor.DARKGRAY;// = los ? GLColor.DARKGREEN : GLColor.DARKRED;
				/*if (!visible){
					bg = los ? GLColor.LIGHTGRAY : GLColor.DARKGRAY;
				} else {
					bg = los ? null : GLColor.DARKRED;
				} */
				if (creature != null) {
					glyphAt(creature.glyph, creature.getFg(), i, j, bg);
				} else if (item != null) {
					glyphAt(item.glyph, item.color, i, j, bg);
				} else {
					glyphAt(tile.glyph, tile.color, i, j, bg);
				}
			}
		}
	}

	private final GLColor[] hpMarks = {GLColor.RED, GLColor.YELLOW, GLColor.GREEN, GLColor.WHITE};

	private int uiInventoryX() {
		return mapCols + 1;
	}

	private int uiInventoryY() {
		return statusRows + 11;
	}

	private int uiInvWidth() {
		return infoCols - 1;
	}

	@SuppressWarnings({"ValueOfIncrementOrDecrementUsed"})
	private void renderInfo() {
		int line = 0;
		// PARAMS
		// Line 0: NAME
		print(1, line++, GLColor.WHITE, player().name);
		// Line 1: AP
		print(1, line++, player().canAct() ? GLColor.WHITE : GLColor.GRAY, "AP: " + player().actionPoints);
		double rel = ((double) player().hitPts) / player().hitPtsMax();
		GLColor hpcolor = (rel > 1.0) ? GLColor.BLUE : hpMarks[(int) (rel * 3)];
		// Line 2: HP
		print(1, line++, hpcolor, "HP: %d/%d", player().hitPts, player().hitPtsMax());
		// Line 4,5: Attributes
		for (int i = 0; i < Attribute.COUNT; i++) {
			print(1 + 3 * i, line + 1, GLColor.WHITE, Attribute.ALL[i].shortName);
			print(1 + 3 * i, line + 2, GLColor.WHITE, "%2d", player().attributes[i]);
		}
		line += 4;
		// INVENTORY
		RLItem weapon1 = player().inventory.getFirst(Creature.InventorySlot.PRIMARY_WEAPON);
		RLItem weapon2 = player().inventory.getFirst(Creature.InventorySlot.SECONDARY_WEAPON);
		RLItem armor = player().inventory.getFirst(Creature.InventorySlot.ARMOR);
		// Line 7: weapon1
		print(1, line++, player().primaryWeaponActive ? GLColor.YELLOW : GLColor.WHITE, "W1:");
		if (weapon1 == null) {
			print(GLColor.WHITE, "  none");
		} else {
			print(weapon1.color, String.valueOf(weapon1.glyph));
			printX++;
			print(GLColor.WHITE, weapon1.displayName());
		}
		// Line 8: weapon2
		print(1, line++, player().primaryWeaponActive ? GLColor.WHITE : GLColor.YELLOW, "W2:");
		if (weapon2 == null) {
			print(GLColor.WHITE, "  none");
		} else {
			print(weapon2.color, String.valueOf(weapon2.glyph));
			printX++;
			print(GLColor.WHITE, weapon2.displayName());
		}
		// Line 9: armor
		print(1, line++, GLColor.WHITE, "Ar:");
		if (armor == null) {
			print(GLColor.WHITE, "  none");
		} else {
			print(armor.color, String.valueOf(armor.glyph));
			printX++;
			print(GLColor.WHITE, armor.displayName());
		}
		// Line 10: backpack
		print(1, line++, GLColor.WHITE, "Bp:  ");
		// Line 11: inventory
		printTo(1, line++);
		int itemCounter = 0;
		for (RLItem item : player().backpack()) {
			print(item.color, String.valueOf(item.glyph));
			itemCounter++;
			if (itemCounter % uiInvWidth() == 0) {
				printX = 1;
				printY = line++;
			}
		}
	}

	private void renderLog() {
		printTo(0, logRows - 1);
		for (Iterator<ColouredText> iterator = world.log.descendingIterator(); iterator.hasNext(); ) {
			ColouredText text = iterator.next();
			for (ColouredString string : text.strings) {
				print(string.color, string.bg, string.string);
			}
			printX = 0;
			printY--;
			if (printY < 0) break;
		}
	}

	private void renderObjects() {
		if (renderCursor) {
			drawLineLoop(
					GLColor.CYAN,
					Pt(curX0 + curX, curY0 + curY),
					Pt(curX0 + curX + 1, curY0 + curY),
					Pt(curX0 + curX + 1, curY0 + curY + 1),
					Pt(curX0 + curX, curY0 + curY + 1)
			);
		}
	}

	@Override
	protected void render() {
		gl.glEnable(GL.GL_TEXTURE_2D);
		scale(CHAR_WIDTH, CHAR_HEIGHT, 1.0);
		glyphtex.bind();

		pushMatrix();
		renderStatus();
		popMatrix();

		pushMatrix();
		translate(0, statusRows, 0);
		renderMap();
		popMatrix();

		pushMatrix();
		translate(mapCols, statusRows, 0);
		renderInfo();
		popMatrix();

		pushMatrix();
		translate(0, logTop(), 0);
		renderLog();
		popMatrix();

		gl.glDisable(GL.GL_TEXTURE_2D);
		renderObjects();

		drawLines(paneBorderColor
				, Pt(0, statusRows), Pt(windowCols, statusRows)
				, Pt(0, logTop()), Pt(windowCols, logTop())
				, Pt(mapCols, statusRows), Pt(mapCols, logTop())
		);

		//worldWork();

		//frame.setTitle(String.format("%.2f",FPS));
	}

	private boolean worldWork() {
		if (!player().canAct()) {
			if (uiMode == UIMode.ACTIVE) uiMode = UIMode.WAIT;
			if (timerTime() >= gameTickMs && !player().canAct()) {
				timerReset();
				world.step();
				if (player().canAct()) {
					fov = player().map().genFOV(player().x(), player().y(), player().sight());
					uiMode = UIMode.ACTIVE;
				}
			}
			return true;
		}
		return false;
	}

	@Override
	protected void customInit() {
		try {
			glyphtex = TextureIO.newTexture(new File(GLYPH_TEXFILE), false);
		} catch (Exception ex) {
			throw new RuntimeException(ex);
		}
	}

	RLWindow() {
		super();
		// render options
		options.windowTitle = "JRoguelike";
		options.windowWidth = 640;
		options.windowHeight = 480;
		options.resizeOnReshape = false;
		// setup fields
		//  interface
		windowRows = options.windowHeight / CHAR_HEIGHT;
		statusRows = 1;
		logRows = 4;
		mapinfoRows = windowRows - (statusRows + logRows);
		windowCols = options.windowWidth / CHAR_WIDTH;
		infoCols = 30;
		mapCols = windowCols - infoCols;
		//  glyphs
		for (int i = 0; i < GLYPH_LIST.length; i++) {
			GLYPH_INDEXES[GLYPH_LIST[i]] = i;
		}
		// other
		world = World.createDefaultWorld();
		uiMode = UIMode.WAIT;
		Thread worldrun = new Thread(new Runnable() {

			@Override
			public void run() {
				while (true) {
					synchronized (RLWindow.this) {
						worldWork();
						if (uiMode == UIMode.ACTIVE) {
							try {
								RLWindow.this.wait(50);
							} catch (InterruptedException ignored) {
							}
						}
					}
					Thread.yield();
				}
			}
		});
		worldrun.setDaemon(true);
		worldrun.start();
	}

	public static void debug(String fmt, Object... args) {
		System.out.printf(fmt, args);
	}

	@SuppressWarnings({"MethodCanBeVariableArityMethod"})
	public static void main(String[] args) {
		Locale.setDefault(Locale.ENGLISH);
		new RLWindow().launch();
		System.exit(0);
	}
}
